#!/usr/bin/python
# -*- coding: utf-8 -*-

import sys, urllib, logging, os.path, codecs, re
logging.basicConfig(level=logging.WARNING)
logger = logging.getLogger('webcurves')

try:
  import json
except ImportError: # json is a part of python since 2.6, 
  import simplejson as json # get simplejson if your python is too old

from PyQt4 import QtCore, QtGui, QtWebKit
from maingui import Ui_webcurves
from setupgui import Ui_setup
import resources_rc

import csvfile

__version__ = "0.2"
__author__ = "havard@gulldahl.no"
__license__ = "GPLv3"
__doc__ = "Create beautiful html graphs you can use on the web. Easy!"

settings = QtCore.QSettings("lurtgjort.no", "webcurves")

FLOT = 1
FLOTR = 2
GVIZ = 3

class jsonFunction(object):
  def __init__(self, code):
    self.code = code

def jsonFormatter(o):
  #logger.debug('jsonFormatter got %s', repr(o))
  if isinstance(o, jsonFunction):
    return "<<func:%s>>" % o.code
  else:
    raise TypeError

def show_gui(args):
  logger.debug("args: %s", args)
  class webcurvesUI(Ui_webcurves):
    def __init__(self, gui):
      self.gui = gui
      self.graf = graf()
      self.setupUi(gui)
      QtCore.QObject.connect(self.tableData, QtCore.SIGNAL('cellChanged(int, int)'), lambda x, y: self.update_data())
      QtCore.QObject.connect(self.codeWindow, QtCore.SIGNAL('textChanged()'), self.update_graph)
      QtCore.QObject.connect(self.textTitle, QtCore.SIGNAL('editingFinished()'), lambda: self.update_config('title', unicode(self.textTitle.text())))
      QtCore.QObject.connect(self.checkBars, QtCore.SIGNAL('toggled(bool)'), lambda b: self.update_config('bars', b))
      QtCore.QObject.connect(self.checkPoints, QtCore.SIGNAL('toggled(bool)'), lambda b: self.update_config('points', b))
      QtCore.QObject.connect(self.checkPie, QtCore.SIGNAL('toggled(bool)'), lambda b: self.update_config('pie', b))
      QtCore.QObject.connect(self.checkLines, QtCore.SIGNAL('toggled(bool)'), lambda b: self.update_config('lines', b))
      QtCore.QObject.connect(self.checkLineFill, QtCore.SIGNAL('toggled(bool)'), lambda b: self.update_config('linefill', b))
      QtCore.QObject.connect(self.checkMousetracker, QtCore.SIGNAL('toggled(bool)'), lambda b: self.update_config('mousetracker', b))
      QtCore.QObject.connect(self.spinYmin, QtCore.SIGNAL('valueChanged(double)'), lambda d: self.update_config('ymin', float(d)))
      QtCore.QObject.connect(self.spinYmax, QtCore.SIGNAL('valueChanged(double)'), lambda d: self.update_config('ymax', float(d)))
      QtCore.QObject.connect(self.textXTitle, QtCore.SIGNAL('editingFinished()'), lambda: self.update_config('xtitle', unicode(self.textXTitle.text())))
      QtCore.QObject.connect(self.textYTitle, QtCore.SIGNAL('editingFinished()'), lambda: self.update_config('ytitle', unicode(self.textYTitle.text())))
      QtCore.QObject.connect(self.spinXLabelsAngle, QtCore.SIGNAL('valueChanged(int)'), lambda i: self.update_config('xlabels_angle', i))
      QtCore.QObject.connect(self.tableData, QtCore.SIGNAL('customContextMenuRequested(QPoint)'), self.show_context_menu)
      QtCore.QObject.connect(self.actionQuit, QtCore.SIGNAL('triggered()'), lambda: self.gui.emit(QtCore.SIGNAL('quit()')))
      QtCore.QObject.connect(self.actionSetup, QtCore.SIGNAL('triggered()'), self.setup_program)
      QtCore.QObject.connect(self.actionCopy_to_clipboard, QtCore.SIGNAL('triggered()'), lambda: self.write_code(to_clipboard=True))
      QtCore.QObject.connect(self.actionSave_to_file, QtCore.SIGNAL('triggered()'), lambda: self.write_code(to_file=True))
      QtCore.QObject.connect(self.actionShow_code, QtCore.SIGNAL('triggered()'), lambda: self.codeSplitter.setSizes( [3, 2] ))
      QtCore.QObject.connect(self.actionSave_image, QtCore.SIGNAL('triggered()'), self.save_graph_image)
      QtCore.QObject.connect(self.actionSave_data, QtCore.SIGNAL('triggered()'), self.write_csv_file)
      QtCore.QObject.connect(self.actionAbout, QtCore.SIGNAL('triggered()'), self.show_about)
      QtCore.QObject.connect(self.actionOpen_CSV, QtCore.SIGNAL('triggered()'), self.read_csv_file)
      
      self.codeSplitter.setSizes( [1, 0] ) # hide codeWindow
      self.moreOptions.setShown(False)
      if os.path.exists('./test.html'): self.grafviewer.setHtml(open('./test.html').read()) # test file
 
    def show_context_menu(self, pos):
      logger.debug("popup at %s, %s", pos.x(), pos.y())
      localpos = self.tableData.mapToGlobal(pos)
      menu = QtGui.QMenu('Setup details', self.tableData)
      menu.addAction("Set row label", self.set_row_label, QtGui.QKeySequence("Ctrl+R"))
      menu.addAction("Set column label", self.set_column_label, QtGui.QKeySequence("Ctrl+N"))
      menu.addSeparator()
      menu.addAction("Remove this row", self.remove_row)
      menu.addAction("Remove this column", self.remove_column)
      menu.exec_(localpos)
    
    def setup_program(self):
      fileloc = 'file://' + module_path()
      flotr_default = settings.value("flotr-location", QtCore.QVariant(fileloc + '/flotr')).toString()
      flot_default = settings.value("flot-location", QtCore.QVariant(fileloc + '/flot')).toString()
      graphlib = settings.value("graphlib", QtCore.QVariant('flot')).toString()
      class setup_dialog(Ui_setup):
        def __init__(self):
          self.gui = QtGui.QDialog()
          self.setupUi(self.gui)
          self.buttons = QtGui.QButtonGroup(self.gui)
          self.buttons.addButton(self.radioFlot, FLOT)
          self.buttons.addButton(self.radioFlotr, FLOTR)
          self.buttons.addButton(self.radioGviz, GVIZ)
          self.flot_location.setText(flot_default)
          self.flotr_location.setText(flotr_default)
          if unicode(graphlib)== "flot": btn = FLOT
          elif unicode(graphlib) == "flotr": btn = FLOTR
          elif unicode(graphlib) == "gviz": btn = GVIZ
          self.buttons.button(btn).setChecked(True)
        def exec_(self):
          res = self.gui.exec_()
          if not res:
            return None
          chosen = self.buttons.checkedId()
          if chosen == FLOT:
            loc = unicode(self.flot_location.text())
          elif chosen == FLOTR:
            loc = unicode(self.flotr_location.text())
          elif chosen == GVIZ:
            loc = unicode('')
          return res, chosen, loc
      dialog = setup_dialog()
      try:
        res, lib, loc = dialog.exec_()
      except TypeError:
        return
      logger.debug("res: %s, graphlib: %s, location: %s", res, lib, loc)
      if lib == FLOT:
        settings.setValue("graphlib", QtCore.QVariant('flot'))
        settings.setValue("flot-location", QtCore.QVariant(loc))
      elif lib == FLOTR:
        settings.setValue("graphlib", QtCore.QVariant('flotr'))
        settings.setValue("flotr-location", QtCore.QVariant(loc))
      elif lib == GVIZ:
        settings.setValue("graphlib", QtCore.QVariant('gviz'))
  
    def  write_code(self, to_clipboard=False, to_file=False):
      code = self.codeWindow.toPlainText()
      logger.debug("code: %s = %s", repr(code), code)
      if to_file:
        filename = QtGui.QFileDialog.getSaveFileName(self.gui, "Save code fragment")
        if not filename: return
        f = codecs.open(unicode(filename).encode(sys.getfilesystemencoding()), "wb", "utf-8")
        f.write(unicode(code))
        f.close()
      else:
        clipboard = QtGui.QApplication.clipboard()
        clipboard.setText(code)
  
    def set_row_label(self):
      row = self.tableData.currentRow()
      try: l = self.tableData.verticalHeaderItem(row).text()
      except AttributeError: l = ""
      label = self.ask("Provide a label for row #%i" % (row+1), l)
#      logger.debug("Got row %i label: %s", row+1, label)
      if label is not None:
        header = QtGui.QTableWidgetItem(label)
        self.tableData.setVerticalHeaderItem(row, header)
        self.update_data()

    def set_column_label(self):
      col = self.tableData.currentColumn()
      try: l = self.tableData.horizontalHeaderItem(col).text()
      except AttributeError: l = ""
      label = self.ask("Provide a label for column #%i" % (col+1), l)
#      logger.debug("Got col %i label: %s", col+1, label)
      if label is not None:
        header = QtGui.QTableWidgetItem(label)
        self.tableData.setHorizontalHeaderItem(col, header)
        self.update_data()
      
    def remove_column(self):
      col = self.tableData.currentColumn()
      if self.yesno("Do you really want to remove column #%i?" % (col+1)):
        self.tableData.removeColumn(col)
        self.update_data()

    def remove_row(self):
      row = self.tableData.currentRow()
      if self.yesno("Do you really want to remove row #%i?" % (row+1)):
        self.tableData.removeRow(row)
        self.update_data()

    def save_graph_image(self):
      raster = QtGui.QPixmap.grabWidget(self.grafviewer)
      filename = QtGui.QFileDialog.getSaveFileName(self.gui, "Save graph", "graph.png")
      if not filename: return # cancelled
      raster.save(filename, "PNG")
      
    def read_csv_file(self, filename=None):
      if filename is None:
        filename = QtGui.QFileDialog.getOpenFileName(self.gui, "Open CSV file")
        if not filename: return # cancelled
      csvreader = csvfile.UnicodeCSVReader(unicode(filename).encode(sys.getfilesystemencoding()))
      self.tableData.clear() # nuke table
      for idx, header in enumerate(csvreader.headers):
        self.tableData.setHorizontalHeaderItem(idx, QtGui.QTableWidgetItem(header))
      for row in csvreader:
        for cell in row:
          self.tableData.setItem(cell.row, cell.idx, QtGui.QTableWidgetItem(cell.value))
      logger.debug("csv series labels: %s", csvreader.series_labels)
      for idx, label in enumerate(csvreader.series_labels):
        self.tableData.setVerticalHeaderItem(idx, QtGui.QTableWidgetItem(label))
      self.update_data()
      
    def write_csv_file(self, filename=None):
      if filename is None:
        filename = QtGui.QFileDialog.getSaveFileName(self.gui, "Save to CSV file")
        if not filename: return # cancelled
      data, headers = self.walk_table()
      writer = csvfile.UnicodeCSVWriter(unicode(filename).encode(sys.getfilesystemencoding()))
      if headers:
        writer.writerow(headers.values())
      for d in data:
        writer.writerow(d.data.values(), d.label)
      
    def walk_table(self):
      data = []
      headers = {}
      for row in range(0, self.tableData.rowCount()):
        #logger.debug("reading row %i", row)
        #logger.debug("data is %s", data)
        d = flotrdata(row+1)
        header = self.tableData.verticalHeaderItem(row)
        if header:
#          logger.debug("setting label for row %i: %s", row+1, unicode(header.text()))
          d.label = unicode(header.text())
        for col in range(0, self.tableData.columnCount()):
          if not col+1 in headers.keys():
            h = self.tableData.horizontalHeaderItem(col)
            if h:
#              logger.debug("setting label for col %i: %s", col+1, unicode(h.text()))
              headers[col+1] = unicode(h.text())
          try:
            t = self.tableData.item(row, col).text()
#            logger.debug("reading col %i,%i: %s", row, col, t)
          except AttributeError:
#            logger.debug("cell %i, %i is empty: finishing row", row, col)
            break # no data in this cell, we're finished on this row
          if len(t) == 0:
            continue
          try:
            d.add_point(col+1, float(t))
          except ValueError, e: # not float()-able
            logger.exception(e)
#        logger.debug("dataset is empty: %s", len(d.data) == 0)
        if not d.empty():
          data.append(d)
        else:
#          logger.debug("all cells in this row (%i) are empty, we are finished", row)
          break # row is empty, it has to be the last data row
      return data, headers
          
    def update_data(self):
      data, headers = self.walk_table()
      self.graf.set_data(data)
      self.graf.column_labels = headers
      self.codeWindow.setPlainText(self.graf.create_graph())

    def update_config(self, key, value):
      #logger.debug("updating config %s=%s", key, value)
      #if not isinstance(value, bool) and not value:
        #return
      if key == "title":
        self.graf.set_title(value)
      elif key == "bars":
        self.graf.show_bars(value)
      elif key == "points":
        self.graf.show_points(value)
      elif key == "pie":
        self.graf.show_pie(value)
      elif key == "lines":
        self.graf.show_lines(value)
      elif key == "linefill":
        self.graf.show_linefill(value)
      elif key == "mousetracker":
        self.graf.use_mousetracker(value)
      elif key == "ymin":
        self.graf.set_ymin(value)
      elif key == "ymax":
        self.graf.set_ymax(value)
      elif key == "xlabels_angle":
        self.graf.set_xlabels_angle(value)
      elif key == "xtitle":
        self.graf.set_xtitle(value)
      elif key == "ytitle":
        self.graf.set_ytitle(value)
      self.codeWindow.setPlainText(self.graf.create_graph())

    def update_graph(self):
      tpl = read_template(':/html/template.html')
      tpl.replace('{{GRAPHTEMPLATE}}', self.codeWindow.toPlainText())
      self.grafviewer.setHtml(tpl)

    def ask(self, question, suggestion=None):
      if suggestion == None: suggestion = ""
      (answer, response) = QtGui.QInputDialog.getText(self.gui, "Quis est is?", question, QtGui.QLineEdit.Normal, suggestion)
      if response != False:
       answer = unicode(answer)
      return answer

    def yesno(self, msg):
      btn = QtGui.QMessageBox.question(self.gui, "Are you sure?", msg, QtGui.QMessageBox.Yes | QtGui.QMessageBox.No, QtGui.QMessageBox.No)
      logger.debug("Got Yes button: %s", btn==QtGui.QMessageBox.Yes)
      return btn == QtGui.QMessageBox.Yes

    def alert(self, msg):
      QtGui.QMessageBox.critical(self.gui, "Error!", msg, QtGui.QMessageBox.Ok)

    def msg(self, msg):
      QtGui.QMessageBox.information(self.gui, "Please take note", msg)

    def show_about(self):
      about = QtGui.QDialog(self.gui)
      text = QtGui.QLabel("""<h1>About webcurves %s</h1>
<p><b>%s</b><br>
<br><i>Copyright &copy; 2009 %s</i>
<br>This is free software. You are free to use and redistribute this program according to the terms of the GPL license, version 3.
<br>For updates, documentation and bug reporting, see <a href="http://webcurves.googlecode.com/">the project pages</a>.
</p>
<p>
The graphs are drawn by a javascript library, <i>flotr</i>, by 2008 Bas Wenneker: <a href="http://www.solutoire.com">solutoire.com</a>
</p>""" % (__version__, __doc__, __author__))
      layout = QtGui.QVBoxLayout()
      layout.addWidget(text)
      about.setLayout(layout)
      text.setStyleSheet("QLabel { width: 200px}")
      about.exec_()

  app = QtGui.QApplication(sys.argv)
  MainWindow = QtGui.QMainWindow()
  ui = webcurvesUI(MainWindow)
  app.connect(MainWindow, QtCore.SIGNAL('quit()'), app, QtCore.SLOT('quit()'))
  MainWindow.show()
  if len(args) > 1 and os.path.exists(args[1]):
    ui.read_csv_file(args[1])
  app.exec_()

class flotrdata(object):
  "Keeps everything related to a series in a graph"
  data = {} # Raw data format of a series:  [x1,y1], [x2,y2], ... ]
  label = None
  bars = {}
  points = {}
  lines = {}
  row = None
  def __init__(self, row):
    self.data = {}
    self.label = None
    self.bars = {}
    self.points = {}
    self.lines = {}
    self.row = row
  def add_point(self, x, y):
#    logger.debug('adding point (%s,%s) to %s', x, y, id(self))
    self.data[x] = y
  def empty(self):
    return len(self.data) == 0
  def to_list(self):
    return [ [x,y] for (x,y) in self.data.iteritems() ]
  def to_json(self):
    r = { 'data': self.to_list() }
    if self.label:
      r['label'] = self.label
    for l,t in ({'bars':self.bars, 'points':self.points, 'lines':self.lines}).iteritems():
      if not t: continue
      r[l] = t
    #logger.debug("object json: %s", r)
    return json.dumps(r)

class graf(object):
  "Responsible for the graph layout, axis and other setup"
  data = []
  data_block = ''
  config = {}
  def __init__(self):
    self.config = {'bars':{}, 'lines':{'show':True}, 'points':{}, 'pie':{}, 'yaxis':{}, 'xaxis':{}, 'grid': {}, 'HtmlText':False, 'mouse': {} }
    self.column_labels = {}
  def set_data(self, data):
    self.data = data
  def set_title(self, t):
    self.config['title'] = t
  def show_bars(self, b):
    self.config['bars']['show'] = b
    self.config['bars']['centered'] = True
    self.config['grid']['verticalLines'] = not b
  def show_points(self, b):
    self.config['points']['show'] = b
  def show_pie(self, b):
    self.config['pie']['show'] = b
    self.config['xaxis']['showLabels'] = not b
    self.config['yaxis']['showLabels'] = not b
    self.config['grid']['verticalLines'] = not b
    self.config['grid']['horizontalLines'] = not b
    self.config['grid']['outlineWidth'] = int(not b) * 2 # line width is 0 or 2 pixels
  def show_lines(self, b):
    self.config['lines']['show'] = b
  def show_linefill(self, b):
    self.config['lines']['fill'] = b
  def use_mousetracker(self, b):
    if not b:
      self.config['mouse'] = {}
    else:
      self.config['mouse']['track'] = True
      self.config['mouse']['position'] = 'ne'
      self.config['mouse']['sensibility'] = 4
      self.config['mouse']['relative'] = True
      #self.config['mouse']['trackFormatter'] = jsonFunction("""function(obj){ return 'x = ' + obj.x +', y = ' + obj.y; }""")
      self.config['mouse']['trackFormatter'] = jsonFunction("mouseTracker")
  def set_ymin(self, d):
    self.config['yaxis']['min'] = d
  def set_ymax(self, d):
    self.config['yaxis']['max'] = d
  def set_xlabels_angle(self, i):
    self.config['xaxis']['labelsAngle'] = i
  def set_xtitle(self, t):
    self.config['xaxis']['title'] = t
  def set_ytitle(self, t):
    self.config['yaxis']['title'] = t
  def create_graph(self):
    code = ", \r\n".join([d.to_json() for d in self.data])
    #logger.debug("Computed js block: %s", code)
    config = []
    if self.column_labels: # special tickmarks for the x-axis
      self.config['xaxis']['ticks'] = [ [x, unicode(tick)] for (x, tick) in self.column_labels.iteritems() ]
    y = self.config['yaxis']
    try:
      if y['min'] == 0.0 and y['max'] == 0.0: # they are both 0.0
        y['min'] = y['max'] = None # special case: auto range
    except KeyError:
      pass
    for k in self.config.keys():
      try:
        config.append("'%s': {%s}" % (k, ",".join("'%s':%s" % (key, json.dumps(val, default=jsonFormatter)) for (key, val) in self.config[k].iteritems())) )
      except AttributeError:
        config.append("'%s': %s" % (k, json.dumps(self.config[k])))
    re_config = re.compile(r"""("|')<<func:([^>]+?)>>("|')""")
    allconfig = ",".join(config)
    try:
      allconfig = re_config.sub(r'\2', allconfig)
    except:
      raise
    logger.debug("graf config: %s", allconfig)
    graphlib = settings.value("graphlib", QtCore.QVariant('flot')).toString()
    #logger.debug("using graphlib: %s", unicode(graphlib))
    s = read_template(':/html/graphtemplate.%s.html' % unicode(graphlib))
    s.replace('{{DRAW_DATA_SERIES}}', unicode(code))
    s.replace('{{CONFIG}}', allconfig)
    return s

def read_template(name):
  tpl = QtCore.QFile(name)
  if not tpl.open(QtCore.QIODevice.ReadOnly | QtCore.QIODevice.Text):
    raise IOError(u"Could not read template '%s' from resource file" % tpl_file)
  t = QtCore.QTextStream(tpl)
  t.setCodec("UTF-8")
  s = QtCore.QString(t.readAll())
  tpl.close()
  ## Template is now in s
  flot_loc = settings.value("flot-location", QtCore.QVariant('file://' + module_path() + '/flot')).toString()
  s.replace('{{FLOTDIR}}', flot_loc)
  flotr_loc = settings.value("flotr-location", QtCore.QVariant('file://' + module_path() + '/flotr')).toString()
  s.replace('{{FLOTRDIR}}', flotr_loc)
  return s

def module_path():
    """ This will get us the program's directory,
    even if we are frozen using py2exe"""
    if hasattr(sys, 'frozen'):
      us = sys.executable
    else:
      us = __file__
    return os.path.abspath(os.path.dirname(us.encode(sys.getfilesystemencoding())))

if __name__ == "__main__":
  if '-d' in sys.argv:
    logger.setLevel(logging.DEBUG)
    print __doc__
    sys.argv.remove('-d')
  else:
    logger.setLevel(logging.ERROR)
    logging.getLogger().setLevel(logging.ERROR)

  if '-v' in sys.argv:
    print "webcurves, version %s\nCopyright 2009 havard@gulldahl.no. Freely distributed by the terms of the GPL3 license" % (__version__)
    sys.exit(0)

  try: # try to read from the command line, no gui
    initials, dep, user, password = sys.argv[1:]
    logger.debug('Console mode, not running gui')
    logger.debug('Finished')
    sys.exit(0)
  except ValueError:
    logger.debug('No arguments or not valid arguments, running gui.')
    sys.exit(show_gui(sys.argv))
  except Exception, (e):
    logger.error(e)
    logger.error('Running gui')
    sys.exit(show_gui(sys.argv))
